Completed
Pull Request — master (#91)
by thomas
01:37
created

Wallet.sendTransaction   B

Complexity

Conditions 3
Paths 3

Size

Total Lines 34

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 3
c 1
b 0
f 1
nc 3
dl 0
loc 34
rs 8.8571
nop 6

2 Functions

Rating   Name   Duplication   Size   Complexity  
A 0 9 3
A 0 3 1
1
var _ = require('lodash');
2
var assert = require('assert');
3
var q = require('q');
4
var async = require('async');
5
var bitcoin = require('bitcoinjs-lib');
6
var bitcoinMessage = require('bitcoinjs-message');
7
var blocktrail = require('./blocktrail');
8
var CryptoJS = require('crypto-js');
9
var Encryption = require('./encryption');
10
var EncryptionMnemonic = require('./encryption_mnemonic');
11
var SizeEstimation = require('./size_estimation');
12
var bip39 = require('bip39');
13
14
var SignMode = {
15
    SIGN: "sign",
16
    DONT_SIGN: "dont_sign"
17
};
18
19
/**
20
 *
21
 * @param sdk                   APIClient       SDK instance used to do requests
22
 * @param identifier            string          identifier of the wallet
23
 * @param walletVersion         string
24
 * @param primaryMnemonic       string          primary mnemonic
25
 * @param encryptedPrimarySeed
26
 * @param encryptedSecret
27
 * @param primaryPublicKeys     string          primary mnemonic
28
 * @param backupPublicKey       string          BIP32 master pubKey M/
29
 * @param blocktrailPublicKeys  array           list of blocktrail pubKeys indexed by keyIndex
30
 * @param keyIndex              int             key index to use
31
 * @param segwit                int             segwit toggle from server
32
 * @param testnet               bool            testnet
33
 * @param checksum              string
34
 * @param upgradeToKeyIndex     int
35
 * @param useNewCashAddr        bool            flag to opt in to bitcoin cash cashaddr's
36
 * @param bypassNewAddressCheck bool            flag to indicate if wallet should/shouldn't derive new address locally to verify api
37
 * @constructor
38
 * @internal
39
 */
40
var Wallet = function(
41
    sdk,
42
    identifier,
43
    walletVersion,
44
    primaryMnemonic,
45
    encryptedPrimarySeed,
46
    encryptedSecret,
47
    primaryPublicKeys,
48
    backupPublicKey,
49
    blocktrailPublicKeys,
50
    keyIndex,
51
    segwit,
52
    testnet,
53
    checksum,
54
    upgradeToKeyIndex,
55
    useNewCashAddr,
56
    bypassNewAddressCheck
57
) {
58
    /* jshint -W071 */
59
    var self = this;
60
61
    self.sdk = sdk;
62
    self.identifier = identifier;
63
    self.walletVersion = walletVersion;
64
    self.locked = true;
65
    self.bypassNewAddressCheck = !!bypassNewAddressCheck;
66
    self.bitcoinCash = self.sdk.bitcoinCash;
67
    self.segwit = !!segwit;
68
    self.useNewCashAddr = !!useNewCashAddr;
69
    assert(!self.segwit || !self.bitcoinCash);
70
71
    self.testnet = testnet;
72
    if (self.bitcoinCash) {
73
        if (self.testnet) {
74
            self.network = bitcoin.networks.bitcoincashtestnet;
75
        } else {
76
            self.network = bitcoin.networks.bitcoincash;
77
        }
78
    } else {
79
        if (self.testnet) {
80
            self.network = bitcoin.networks.testnet;
81
        } else {
82
            self.network = bitcoin.networks.bitcoin;
83
        }
84
    }
85
86
    assert(backupPublicKey instanceof bitcoin.HDNode);
87
    assert(_.every(primaryPublicKeys, function(primaryPublicKey) { return primaryPublicKey instanceof bitcoin.HDNode; }));
88
    assert(_.every(blocktrailPublicKeys, function(blocktrailPublicKey) { return blocktrailPublicKey instanceof bitcoin.HDNode; }));
89
90
    // v1
91
    self.primaryMnemonic = primaryMnemonic;
92
93
    // v2 & v3
94
    self.encryptedPrimarySeed = encryptedPrimarySeed;
95
    self.encryptedSecret = encryptedSecret;
96
97
    self.primaryPrivateKey = null;
98
    self.backupPrivateKey = null;
99
100
    self.backupPublicKey = backupPublicKey;
101
    self.blocktrailPublicKeys = blocktrailPublicKeys;
102
    self.primaryPublicKeys = primaryPublicKeys;
103
    self.keyIndex = keyIndex;
104
105
    if (!self.bitcoinCash) {
106
        if (self.segwit) {
107
            self.chain = Wallet.CHAIN_BTC_DEFAULT;
108
            self.changeChain = Wallet.CHAIN_BTC_SEGWIT;
109
        } else {
110
            self.chain = Wallet.CHAIN_BTC_DEFAULT;
111
            self.changeChain = Wallet.CHAIN_BTC_DEFAULT;
112
        }
113
    } else {
114
        self.chain = Wallet.CHAIN_BCC_DEFAULT;
115
        self.changeChain = Wallet.CHAIN_BCC_DEFAULT;
116
    }
117
118
    self.checksum = checksum;
119
    self.upgradeToKeyIndex = upgradeToKeyIndex;
120
121
    self.secret = null;
122
    self.seedHex = null;
123
};
124
125
Wallet.WALLET_VERSION_V1 = 'v1';
126
Wallet.WALLET_VERSION_V2 = 'v2';
127
Wallet.WALLET_VERSION_V3 = 'v3';
128
129
Wallet.WALLET_ENTROPY_BITS = 256;
130
131
Wallet.OP_RETURN = 'opreturn';
132
Wallet.DATA = Wallet.OP_RETURN; // alias
133
134
Wallet.PAY_PROGRESS_START = 0;
135
Wallet.PAY_PROGRESS_COIN_SELECTION = 10;
136
Wallet.PAY_PROGRESS_CHANGE_ADDRESS = 20;
137
Wallet.PAY_PROGRESS_SIGN = 30;
138
Wallet.PAY_PROGRESS_SEND = 40;
139
Wallet.PAY_PROGRESS_DONE = 100;
140
141
Wallet.CHAIN_BTC_DEFAULT = 0;
142
Wallet.CHAIN_BTC_SEGWIT = 2;
143
Wallet.CHAIN_BCC_DEFAULT = 1;
144
145
Wallet.FEE_STRATEGY_FORCE_FEE = blocktrail.FEE_STRATEGY_FORCE_FEE;
146
Wallet.FEE_STRATEGY_BASE_FEE = blocktrail.FEE_STRATEGY_BASE_FEE;
147
Wallet.FEE_STRATEGY_HIGH_PRIORITY = blocktrail.FEE_STRATEGY_HIGH_PRIORITY;
148
Wallet.FEE_STRATEGY_OPTIMAL = blocktrail.FEE_STRATEGY_OPTIMAL;
149
Wallet.FEE_STRATEGY_LOW_PRIORITY = blocktrail.FEE_STRATEGY_LOW_PRIORITY;
150
Wallet.FEE_STRATEGY_MIN_RELAY_FEE = blocktrail.FEE_STRATEGY_MIN_RELAY_FEE;
151
152
Wallet.prototype.isSegwit = function() {
153
    return !!this.segwit;
154
};
155
156
Wallet.prototype.unlock = function(options, cb) {
157
    var self = this;
158
159
    var deferred = q.defer();
160
    deferred.promise.nodeify(cb);
161
162
    // avoid modifying passed options
163
    options = _.merge({}, options);
164
165
    q.fcall(function() {
166
        switch (self.walletVersion) {
167
            case Wallet.WALLET_VERSION_V1:
0 ignored issues
show
Bug introduced by
The variable Wallet seems to be never declared. If this is a global, consider adding a /** global: Wallet */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
168
                return self.unlockV1(options);
169
170
            case Wallet.WALLET_VERSION_V2:
171
                return self.unlockV2(options);
172
173
            case Wallet.WALLET_VERSION_V3:
174
                return self.unlockV3(options);
175
176
            default:
177
                return q.reject(new blocktrail.WalletInitError("Invalid wallet version"));
178
        }
179
    }).then(
180
        function(primaryPrivateKey) {
181
            self.primaryPrivateKey = primaryPrivateKey;
182
183
            // create a checksum of our private key which we'll later use to verify we used the right password
184
            var checksum = self.primaryPrivateKey.getAddress();
185
186
            // check if we've used the right passphrase
187
            if (checksum !== self.checksum) {
188
                throw new blocktrail.WalletChecksumError("Generated checksum [" + checksum + "] does not match " +
189
                    "[" + self.checksum + "], most likely due to incorrect password");
190
            }
191
192
            self.locked = false;
193
194
            // if the response suggests we should upgrade to a different blocktrail cosigning key then we should
195
            if (typeof self.upgradeToKeyIndex !== "undefined" && self.upgradeToKeyIndex !== null) {
0 ignored issues
show
Complexity Best Practice introduced by
There is no return statement if typeof self.upgradeToKey...radeToKeyIndex !== null is false. Are you sure this is correct? If so, consider adding return; explicitly.

This check looks for functions where a return statement is found in some execution paths, but not in all.

Consider this little piece of code

function isBig(a) {
    if (a > 5000) {
        return "yes";
    }
}

console.log(isBig(5001)); //returns yes
console.log(isBig(42)); //returns undefined

The function isBig will only return a specific value when its parameter is bigger than 5000. In any other case, it will implicitly return undefined.

This behaviour may not be what you had intended. In any case, you can add a return undefined to the other execution path to make the return value explicit.

Loading history...
196
                return self.upgradeKeyIndex(self.upgradeToKeyIndex);
197
            }
198
        }
199
    ).then(
200
        function(r) {
201
            deferred.resolve(r);
202
        },
203
        function(e) {
204
            deferred.reject(e);
205
        }
206
    );
207
208
    return deferred.promise;
209
};
210
211
Wallet.prototype.unlockV1 = function(options) {
212
    var self = this;
213
214
    options.primaryMnemonic = typeof options.primaryMnemonic !== "undefined" ? options.primaryMnemonic : self.primaryMnemonic;
215
    options.secretMnemonic = typeof options.secretMnemonic !== "undefined" ? options.secretMnemonic : self.secretMnemonic;
216
217
    return self.sdk.resolvePrimaryPrivateKeyFromOptions(options)
218
        .then(function(options) {
219
            self.primarySeed = options.primarySeed;
220
221
            return options.primaryPrivateKey;
222
        });
223
};
224
225
Wallet.prototype.unlockV2 = function(options, cb) {
226
    var self = this;
227
228
    var deferred = q.defer();
229
    deferred.promise.nodeify(cb);
230
231
    deferred.resolve(q.fcall(function() {
232
        /* jshint -W071, -W074 */
233
        options.encryptedPrimarySeed = typeof options.encryptedPrimarySeed !== "undefined" ? options.encryptedPrimarySeed : self.encryptedPrimarySeed;
234
        options.encryptedSecret = typeof options.encryptedSecret !== "undefined" ? options.encryptedSecret : self.encryptedSecret;
235
236
        if (options.secret) {
237
            self.secret = options.secret;
238
        }
239
240
        if (options.primaryPrivateKey) {
241
            throw new blocktrail.WalletDecryptError("specifying primaryPrivateKey has been deprecated");
242
        }
243
244
        if (options.primarySeed) {
245
            self.primarySeed = options.primarySeed;
246
        } else if (options.secret) {
247
            try {
248
                self.primarySeed = new Buffer(
0 ignored issues
show
Bug introduced by
The variable Buffer seems to be never declared. If this is a global, consider adding a /** global: Buffer */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
249
                    CryptoJS.AES.decrypt(CryptoJS.format.OpenSSL.parse(options.encryptedPrimarySeed), self.secret).toString(CryptoJS.enc.Utf8), 'base64');
250
                if (!self.primarySeed.length) {
251
                    throw new Error();
252
                }
253
            } catch (e) {
254
                throw new blocktrail.WalletDecryptError("Failed to decrypt primarySeed");
255
            }
256
257
        } else {
258
            // avoid conflicting options
259
            if (options.passphrase && options.password) {
260
                throw new blocktrail.WalletCreateError("Can't specify passphrase and password");
261
            }
262
            // normalize passphrase/password
263
            options.passphrase = options.passphrase || options.password;
264
265
            try {
266
                self.secret = CryptoJS.AES.decrypt(CryptoJS.format.OpenSSL.parse(options.encryptedSecret), options.passphrase).toString(CryptoJS.enc.Utf8);
267
                if (!self.secret.length) {
268
                    throw new Error();
269
                }
270
            } catch (e) {
271
                throw new blocktrail.WalletDecryptError("Failed to decrypt secret");
272
            }
273
            try {
274
                self.primarySeed = new Buffer(
275
                    CryptoJS.AES.decrypt(CryptoJS.format.OpenSSL.parse(options.encryptedPrimarySeed), self.secret).toString(CryptoJS.enc.Utf8), 'base64');
276
                if (!self.primarySeed.length) {
277
                    throw new Error();
278
                }
279
            } catch (e) {
280
                throw new blocktrail.WalletDecryptError("Failed to decrypt primarySeed");
281
            }
282
        }
283
284
        return bitcoin.HDNode.fromSeedBuffer(self.primarySeed, self.network);
285
    }));
286
287
    return deferred.promise;
288
};
289
290
Wallet.prototype.unlockV3 = function(options, cb) {
291
    var self = this;
292
293
    var deferred = q.defer();
294
    deferred.promise.nodeify(cb);
295
296
    deferred.resolve(q.fcall(function() {
297
        return q.when()
298
            .then(function() {
299
                /* jshint -W071, -W074 */
300
                options.encryptedPrimarySeed = typeof options.encryptedPrimarySeed !== "undefined" ? options.encryptedPrimarySeed : self.encryptedPrimarySeed;
301
                options.encryptedSecret = typeof options.encryptedSecret !== "undefined" ? options.encryptedSecret : self.encryptedSecret;
302
303
                if (options.secret) {
304
                    self.secret = options.secret;
305
                }
306
307
                if (options.primaryPrivateKey) {
308
                    throw new blocktrail.WalletInitError("specifying primaryPrivateKey has been deprecated");
309
                }
310
311
                if (options.primarySeed) {
312
                    self.primarySeed = options.primarySeed;
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
313
                } else if (options.secret) {
314
                    return self.sdk.promisedDecrypt(new Buffer(options.encryptedPrimarySeed, 'base64'), self.secret)
0 ignored issues
show
Bug introduced by
The variable Buffer seems to be never declared. If this is a global, consider adding a /** global: Buffer */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
315
                        .then(function(primarySeed) {
316
                            self.primarySeed = primarySeed;
317
                        }, function() {
318
                            throw new blocktrail.WalletDecryptError("Failed to decrypt primarySeed");
319
                        });
320
                } else {
321
                    // avoid conflicting options
322
                    if (options.passphrase && options.password) {
323
                        throw new blocktrail.WalletCreateError("Can't specify passphrase and password");
324
                    }
325
                    // normalize passphrase/password
326
                    options.passphrase = options.passphrase || options.password;
327
                    delete options.password;
328
329
                    return self.sdk.promisedDecrypt(new Buffer(options.encryptedSecret, 'base64'), new Buffer(options.passphrase))
0 ignored issues
show
Bug introduced by
The variable Buffer seems to be never declared. If this is a global, consider adding a /** global: Buffer */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
330
                        .then(function(secret) {
331
                            self.secret = secret;
332
                        }, function() {
333
                            throw new blocktrail.WalletDecryptError("Failed to decrypt secret");
334
                        })
335
                        .then(function() {
336
                            return self.sdk.promisedDecrypt(new Buffer(options.encryptedPrimarySeed, 'base64'), self.secret)
0 ignored issues
show
Bug introduced by
The variable Buffer seems to be never declared. If this is a global, consider adding a /** global: Buffer */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
337
                                .then(function(primarySeed) {
338
                                    self.primarySeed = primarySeed;
339
                                }, function() {
340
                                    throw new blocktrail.WalletDecryptError("Failed to decrypt primarySeed");
341
                                });
342
                        });
343
                }
344
            })
345
            .then(function() {
346
                return bitcoin.HDNode.fromSeedBuffer(self.primarySeed, self.network);
347
            })
348
        ;
349
    }));
350
351
    return deferred.promise;
352
};
353
354
Wallet.prototype.lock = function() {
355
    var self = this;
356
357
    self.secret = null;
358
    self.primarySeed = null;
359
    self.primaryPrivateKey = null;
360
    self.backupPrivateKey = null;
361
362
    self.locked = true;
363
};
364
365
/**
366
 * upgrade wallet to V3 encryption scheme
367
 *
368
 * @param passphrase is required again to reencrypt the data, important that it's the correct password!!!
369
 * @param cb
370
 * @returns {promise}
371
 */
372
Wallet.prototype.upgradeToV3 = function(passphrase, cb) {
373
    var self = this;
374
375
    var deferred = q.defer();
376
    deferred.promise.nodeify(cb);
377
378
    q.when(true)
379
        .then(function() {
380
            if (self.locked) {
381
                throw new blocktrail.WalletLockedError("Wallet needs to be unlocked to upgrade");
382
            }
383
384
            if (self.walletVersion === Wallet.WALLET_VERSION_V3) {
385
                throw new blocktrail.WalletUpgradeError("Wallet is already V3");
386
            } else if (self.walletVersion === Wallet.WALLET_VERSION_V2) {
387
                return self._upgradeV2ToV3(passphrase, deferred.notify.bind(deferred));
388
            } else if (self.walletVersion === Wallet.WALLET_VERSION_V1) {
0 ignored issues
show
Complexity Best Practice introduced by
There is no return statement if self.walletVersion === Wallet.WALLET_VERSION_V1 is false. Are you sure this is correct? If so, consider adding return; explicitly.

This check looks for functions where a return statement is found in some execution paths, but not in all.

Consider this little piece of code

function isBig(a) {
    if (a > 5000) {
        return "yes";
    }
}

console.log(isBig(5001)); //returns yes
console.log(isBig(42)); //returns undefined

The function isBig will only return a specific value when its parameter is bigger than 5000. In any other case, it will implicitly return undefined.

This behaviour may not be what you had intended. In any case, you can add a return undefined to the other execution path to make the return value explicit.

Loading history...
389
                return self._upgradeV1ToV3(passphrase, deferred.notify.bind(deferred));
390
            }
391
        })
392
        .then(function(r) { deferred.resolve(r); }, function(e) { deferred.reject(e); });
393
394
    return deferred.promise;
395
};
396
397
Wallet.prototype._upgradeV2ToV3 = function(passphrase, notify) {
398
    var self = this;
399
400
    return q.when(true)
401
        .then(function() {
402
            var options = {
403
                storeDataOnServer: true,
404
                passphrase: passphrase,
405
                primarySeed: self.primarySeed,
406
                recoverySecret: false // don't create new recovery secret, V2 already has ones
407
            };
408
409
            return self.sdk.produceEncryptedDataV3(options, notify || function noop() {})
410
                .then(function(options) {
411
                    return self.sdk.updateWallet(self.identifier, {
412
                        encrypted_primary_seed: options.encryptedPrimarySeed.toString('base64'),
413
                        encrypted_secret: options.encryptedSecret.toString('base64'),
414
                        wallet_version: Wallet.WALLET_VERSION_V3
415
                    }).then(function() {
416 View Code Duplication
                        self.secret = options.secret;
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
417
                        self.encryptedPrimarySeed = options.encryptedPrimarySeed;
418
                        self.encryptedSecret = options.encryptedSecret;
419
                        self.walletVersion = Wallet.WALLET_VERSION_V3;
420
421
                        return self;
422
                    });
423
                });
424
        });
425
426
};
427
428
Wallet.prototype._upgradeV1ToV3 = function(passphrase, notify) {
429
    var self = this;
430
431
    return q.when(true)
432
        .then(function() {
433
            var options = {
434
                storeDataOnServer: true,
435
                passphrase: passphrase,
436
                primarySeed: self.primarySeed
437
            };
438
439
            return self.sdk.produceEncryptedDataV3(options, notify || function noop() {})
440
                .then(function(options) {
441
                    // store recoveryEncryptedSecret for printing on backup sheet
442
                    self.recoveryEncryptedSecret = options.recoveryEncryptedSecret;
443
444
                    return self.sdk.updateWallet(self.identifier, {
445
                        primary_mnemonic: '',
446
                        encrypted_primary_seed: options.encryptedPrimarySeed.toString('base64'),
447
                        encrypted_secret: options.encryptedSecret.toString('base64'),
448
                        recovery_secret: options.recoverySecret.toString('hex'),
449
                        wallet_version: Wallet.WALLET_VERSION_V3
450
                    }).then(function() {
451
                        self.secret = options.secret;
452
                        self.encryptedPrimarySeed = options.encryptedPrimarySeed;
453
                        self.encryptedSecret = options.encryptedSecret;
454
                        self.walletVersion = Wallet.WALLET_VERSION_V3;
455
456
                        return self;
457
                    });
458
                });
459
        });
460
};
461
462
Wallet.prototype.doPasswordChange = function(newPassword) {
463
    var self = this;
464
465
    return q.when(null)
466
        .then(function() {
467
468
            if (self.walletVersion === Wallet.WALLET_VERSION_V1) {
469
                throw new blocktrail.WalletLockedError("Wallet version does not support password change!");
470
            }
471
472
            if (self.locked) {
473
                throw new blocktrail.WalletLockedError("Wallet needs to be unlocked to change password");
474
            }
475
476
            if (!self.secret) {
477
                throw new blocktrail.WalletLockedError("No secret");
478
            }
479
480
            var newEncryptedSecret;
481
            var newEncrypedWalletSecretMnemonic;
482
            if (self.walletVersion === Wallet.WALLET_VERSION_V2) {
483
                newEncryptedSecret = CryptoJS.AES.encrypt(self.secret, newPassword).toString(CryptoJS.format.OpenSSL);
484
                newEncrypedWalletSecretMnemonic = bip39.entropyToMnemonic(blocktrail.convert(newEncryptedSecret, 'base64', 'hex'));
485
486
            } else {
487
                if (typeof newPassword === "string") {
488
                    newPassword = new Buffer(newPassword);
0 ignored issues
show
Bug introduced by
The variable Buffer seems to be never declared. If this is a global, consider adding a /** global: Buffer */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
489
                } else {
490
                    if (!(newPassword instanceof Buffer)) {
491
                        throw new Error('New password must be provided as a string or a Buffer');
492
                    }
493
                }
494
495
                newEncryptedSecret = Encryption.encrypt(self.secret, newPassword);
496
                newEncrypedWalletSecretMnemonic = EncryptionMnemonic.encode(newEncryptedSecret);
497
498
                // It's a buffer, so convert it back to base64
499
                newEncryptedSecret = newEncryptedSecret.toString('base64');
500
            }
501
502
            return [newEncryptedSecret, newEncrypedWalletSecretMnemonic];
503
        });
504
};
505
506
Wallet.prototype.passwordChange = function(newPassword, cb) {
507
    var self = this;
508
509
    var deferred = q.defer();
510
    deferred.promise.nodeify(cb);
511
512
    q.fcall(function() {
513
        return self.doPasswordChange(newPassword)
514
            .then(function(r) {
515
                var newEncryptedSecret = r[0];
516
                var newEncrypedWalletSecretMnemonic = r[1];
517
518
                return self.sdk.updateWallet(self.identifier, {encrypted_secret: newEncryptedSecret}).then(function() {
519
                    self.encryptedSecret = newEncryptedSecret;
520
521
                    // backupInfo
522
                    return {
523
                        encryptedSecret: newEncrypedWalletSecretMnemonic
524
                    };
525
                });
526
            })
527
            .then(
528
                function(r) {
529
                    deferred.resolve(r);
530
                },
531
                function(e) {
532
                    deferred.reject(e);
533
                }
534
            );
535
    });
536
537
    return deferred.promise;
538
};
539
540
/**
541
 * get address for specified path
542
 *
543
 * @param path
544
 * @returns string
545
 */
546
Wallet.prototype.getAddressByPath = function(path) {
547
    return this.getWalletScriptByPath(path).address;
548
};
549
550
/**
551
 * get redeemscript for specified path
552
 *
553
 * @param path
554
 * @returns {Buffer}
555
 */
556
Wallet.prototype.getRedeemScriptByPath = function(path) {
557
    return this.getWalletScriptByPath(path).redeemScript;
558
};
559
560
/**
561
 * Generate scripts, and address.
562
 * @param path
563
 * @returns {{witnessScript: *, redeemScript: *, scriptPubKey, address: *}}
564
 */
565
Wallet.prototype.getWalletScriptByPath = function(path) {
566
    var self = this;
567
568
    // get derived primary key
569
    var derivedPrimaryPublicKey = self.getPrimaryPublicKey(path);
570
    // get derived blocktrail key
571
    var derivedBlocktrailPublicKey = self.getBlocktrailPublicKey(path);
572
    // derive the backup key
573
    var derivedBackupPublicKey = Wallet.deriveByPath(self.backupPublicKey, path.replace("'", ""), "M");
574
575
    // sort the pubkeys
576
    var pubKeys = Wallet.sortMultiSigKeys([
577
        derivedPrimaryPublicKey.keyPair.getPublicKeyBuffer(),
578
        derivedBackupPublicKey.keyPair.getPublicKeyBuffer(),
579
        derivedBlocktrailPublicKey.keyPair.getPublicKeyBuffer()
580
    ]);
581
582
    var multisig = bitcoin.script.multisig.output.encode(2, pubKeys);
583
    var scriptType = parseInt(path.split("/")[2]);
584
585
    var ws, rs;
586
    if (this.network !== "bitcoincash" && scriptType === Wallet.CHAIN_BTC_SEGWIT) {
587
        ws = multisig;
588
        rs = bitcoin.script.witnessScriptHash.output.encode(bitcoin.crypto.sha256(ws));
589
    } else {
590
        ws = null;
591
        rs = multisig;
592
    }
593
594
    var spk = bitcoin.script.scriptHash.output.encode(bitcoin.crypto.hash160(rs));
595
    var addr = bitcoin.address.fromOutputScript(spk, this.network, self.useNewCashAddr);
596
597
    return {
598
        witnessScript: ws,
599
        redeemScript: rs,
600
        scriptPubKey: spk,
601
        address: addr
602
    };
603
};
604
605
/**
606
 * get primary public key by path
607
 *  first level of the path is used as keyIndex to find the correct key in the dict
608
 *
609
 * @param path  string
610
 * @returns {bitcoin.HDNode}
611
 */
612
Wallet.prototype.getPrimaryPublicKey = function(path) {
613
    var self = this;
614
615
    path = path.replace("m", "M");
616
617
    var keyIndex = path.split("/")[1].replace("'", "");
618
619
    if (!self.primaryPublicKeys[keyIndex]) {
620
        if (self.primaryPrivateKey) {
621
            self.primaryPublicKeys[keyIndex] = Wallet.deriveByPath(self.primaryPrivateKey, "M/" + keyIndex + "'", "m");
622
        } else {
623
            throw new blocktrail.KeyPathError("Wallet.getPrimaryPublicKey keyIndex (" + keyIndex + ") is unknown to us");
624
        }
625
    }
626
627
    var primaryPublicKey = self.primaryPublicKeys[keyIndex];
628
    return Wallet.deriveByPath(primaryPublicKey, path, "M/" + keyIndex + "'");
629
};
630
631
/**
632
 * get blocktrail public key by path
633
 *  first level of the path is used as keyIndex to find the correct key in the dict
634
 *
635
 * @param path  string
636
 * @returns {bitcoin.HDNode}
637
 */
638
Wallet.prototype.getBlocktrailPublicKey = function(path) {
639
    var self = this;
640
641
    path = path.replace("m", "M");
642
643
    var keyIndex = path.split("/")[1].replace("'", "");
644
645
    if (!self.blocktrailPublicKeys[keyIndex]) {
646
        throw new blocktrail.KeyPathError("Wallet.getBlocktrailPublicKey keyIndex (" + keyIndex + ") is unknown to us");
647
    }
648
649
    var blocktrailPublicKey = self.blocktrailPublicKeys[keyIndex];
650
651
    return Wallet.deriveByPath(blocktrailPublicKey, path, "M/" + keyIndex + "'");
652
};
653
654
/**
655
 * upgrade wallet to different blocktrail cosign key
656
 *
657
 * @param keyIndex  int
658
 * @param [cb]      function
659
 * @returns {q.Promise}
660
 */
661
Wallet.prototype.upgradeKeyIndex = function(keyIndex, cb) {
662
    var self = this;
663
664
    var deferred = q.defer();
665
    deferred.promise.nodeify(cb);
666
667
    if (self.locked) {
668
        deferred.reject(new blocktrail.WalletLockedError("Wallet needs to be unlocked to upgrade key index"));
669
        return deferred.promise;
670
    }
671
672
    var primaryPublicKey = self.primaryPrivateKey.deriveHardened(keyIndex).neutered();
673
674
    deferred.resolve(
675
        self.sdk.upgradeKeyIndex(self.identifier, keyIndex, [primaryPublicKey.toBase58(), "M/" + keyIndex + "'"])
676
            .then(function(result) {
677
                self.keyIndex = keyIndex;
678
                _.forEach(result.blocktrail_public_keys, function(publicKey, keyIndex) {
679
                    self.blocktrailPublicKeys[keyIndex] = bitcoin.HDNode.fromBase58(publicKey[0], self.network);
680
                });
681
682
                self.primaryPublicKeys[keyIndex] = primaryPublicKey;
683
684
                return true;
685
            })
686
    );
687
688
    return deferred.promise;
689
};
690
691
/**
692
 * generate a new derived private key and return the new address for it
693
 *
694
 * @param [chainIdx] int
695
 * @param [cb]  function        callback(err, address)
696
 * @returns {q.Promise}
697
 */
698
Wallet.prototype.getNewAddress = function(chainIdx, cb) {
699
    var self = this;
700
701
    // chainIdx is optional
702
    if (typeof chainIdx === "function") {
703
        cb = chainIdx;
704
        chainIdx = null;
705
    }
706
707
    var deferred = q.defer();
708
    deferred.promise.spreadNodeify(cb);
709
710
    // Only enter if it's not an integer
711
    if (chainIdx !== parseInt(chainIdx, 10)) {
712
        // deal with undefined or null, assume defaults
713
        if (typeof chainIdx === "undefined" || chainIdx === null) {
714
            chainIdx = self.chain;
715
        } else {
716
            // was a variable but not integer
717
            deferred.reject(new Error("Invalid chain index"));
718
            return deferred.promise;
719
        }
720
    }
721
722
    deferred.resolve(
723
        self.sdk.getNewDerivation(self.identifier, "M/" + self.keyIndex + "'/" + chainIdx)
724
            .then(function(newDerivation) {
725
                var path = newDerivation.path;
726
                var addressFromServer = newDerivation.address;
727
                var decodedFromServer;
728
729
                try {
730
                    // Decode the address the serer gave us
731
                    decodedFromServer = self.decodeAddress(addressFromServer);
732
                    if ("cashAddrPrefix" in self.network && self.useNewCashAddr && decodedFromServer.type === "base58") {
733
                        self.bypassNewAddressCheck = false;
734
                    }
735
                } catch (e) {
736
                    throw new blocktrail.WalletAddressError("Failed to decode address [" + newDerivation.address + "]");
737
                }
738
739
                if (!self.bypassNewAddressCheck) {
740
                    // We need to reproduce this address with the same path,
741
                    // but the server (for BCH cashaddrs) uses base58?
742
                    var verifyAddress = self.getAddressByPath(newDerivation.path);
743
744
                    // If this occasion arises:
745
                    if ("cashAddrPrefix" in self.network && self.useNewCashAddr && decodedFromServer.type === "base58") {
746
                        // Decode our the address we produced for the path
747
                        var decodeOurs;
748
                        try {
749
                            decodeOurs = self.decodeAddress(verifyAddress);
750
                        } catch (e) {
751
                            throw new blocktrail.WalletAddressError("Error while verifying address from server [" + e.message + "]");
752
                        }
753
754
                        // Peek beyond the encoding - the hashes must match at least
755
                        if (decodeOurs.decoded.hash.toString('hex') !== decodedFromServer.decoded.hash.toString('hex')) {
756
                            throw new blocktrail.WalletAddressError("Failed to verify legacy address [hash mismatch]");
757
                        }
758
759
                        var matchedP2PKH = decodeOurs.decoded.version === bitcoin.script.types.P2PKH &&
760
                            decodedFromServer.decoded.version === self.network.pubKeyHash;
761
                        var matchedP2SH = decodeOurs.decoded.version === bitcoin.script.types.P2SH &&
762
                            decodedFromServer.decoded.version === self.network.scriptHash;
763
764
                        if (!(matchedP2PKH || matchedP2SH)) {
765
                            throw new blocktrail.WalletAddressError("Failed to verify legacy address [prefix mismatch]");
766
                        }
767
768
                        // We are satisfied that the address is for the same
769
                        // destination, so substitute addressFromServer with our
770
                        // 'reencoded' form.
771
                        addressFromServer = decodeOurs.address;
772
                    }
773
774
                    // debug check
775
                    if (verifyAddress !== addressFromServer) {
776
                        throw new blocktrail.WalletAddressError("Failed to verify address [" + newDerivation.address + "] !== [" + addressFromServer + "]");
777
                    }
778
                }
779
780
                return [addressFromServer, path];
781
            })
782
    );
783
784
    return deferred.promise;
785
};
786
787
/**
788
 * get the balance for the wallet
789 View Code Duplication
 *
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
790
 * @param [cb]  function        callback(err, confirmed, unconfirmed)
791
 * @returns {q.Promise}
792
 */
793
Wallet.prototype.getBalance = function(cb) {
794
    var self = this;
795
796
    var deferred = q.defer();
797
    deferred.promise.spreadNodeify(cb);
798
799
    deferred.resolve(
800
        self.sdk.getWalletBalance(self.identifier)
801
            .then(function(result) {
802
                return [result.confirmed, result.unconfirmed];
803
            })
804
    );
805
806
    return deferred.promise;
807
};
808
809
/**
810
 * get the balance for the wallet
811
 *
812
 * @param [cb]  function        callback(err, confirmed, unconfirmed)
813
 * @returns {q.Promise}
814
 */
815
Wallet.prototype.getInfo = function(cb) {
816
    var self = this;
817
818
    var deferred = q.defer();
819
    deferred.promise.spreadNodeify(cb);
820
821
    deferred.resolve(
822
        self.sdk.getWalletBalance(self.identifier)
823
    );
824
825
    return deferred.promise;
826
};
827
828
/**
829
 * do wallet discovery (slow)
830
 *
831
 * @param [gap] int             gap limit
832
 * @param [cb]  function        callback(err, confirmed, unconfirmed)
833
 * @returns {q.Promise}
834
 */
835
Wallet.prototype.doDiscovery = function(gap, cb) {
836
    var self = this;
837
838
    if (typeof gap === "function") {
839
        cb = gap;
840
        gap = null;
841
    }
842
843
    var deferred = q.defer();
844
    deferred.promise.spreadNodeify(cb);
845
846
    deferred.resolve(
847
        self.sdk.doWalletDiscovery(self.identifier, gap)
848
            .then(function(result) {
849
                return [result.confirmed, result.unconfirmed];
850
            })
851
    );
852
853
    return deferred.promise;
854
};
855
856
/**
857
 *
858
 * @param [force]   bool            ignore warnings (such as non-zero balance)
859
 * @param [cb]      function        callback(err, success)
860
 * @returns {q.Promise}
861
 */
862
Wallet.prototype.deleteWallet = function(force, cb) {
863
    var self = this;
864
865
    if (typeof force === "function") {
866
        cb = force;
867 View Code Duplication
        force = false;
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
868
    }
869
870
    var deferred = q.defer();
871
    deferred.promise.nodeify(cb);
872
873
    if (self.locked) {
874
        deferred.reject(new blocktrail.WalletDeleteError("Wallet needs to be unlocked to delete wallet"));
875
        return deferred.promise;
876
    }
877
878
    var checksum = self.primaryPrivateKey.getAddress();
879
    var privBuf = self.primaryPrivateKey.keyPair.d.toBuffer(32);
880
    var signature = bitcoinMessage.sign(checksum, self.network.messagePrefix, privBuf, true).toString('base64');
881
882
    deferred.resolve(
883
        self.sdk.deleteWallet(self.identifier, checksum, signature, force)
884
            .then(function(result) {
885
                return result.deleted;
886
            })
887
    );
888
889
    return deferred.promise;
890
};
891
892
/**
893
 * create, sign and send a transaction
894
 *
895
 * @param pay                   array       {'address': (int)value}     coins to send
896
 * @param [changeAddress]       bool        change address to use (auto generated if NULL)
897
 * @param [allowZeroConf]       bool        allow zero confirmation unspent outputs to be used in coin selection
898
 * @param [randomizeChangeIdx]  bool        randomize the index of the change output (default TRUE, only disable if you have a good reason to)
899
 * @param [feeStrategy]         string      defaults to Wallet.FEE_STRATEGY_OPTIMAL
900
 * @param [twoFactorToken]      string      2FA token
901
 * @param options
902
 * @param [cb]                  function    callback(err, txHash)
903
 * @returns {q.Promise}
904
 */
905
Wallet.prototype.pay = function(pay, changeAddress, allowZeroConf, randomizeChangeIdx, feeStrategy, twoFactorToken, options, cb) {
906
907
    /* jshint -W071 */
908
    var self = this;
909
910
    if (typeof changeAddress === "function") {
911
        cb = changeAddress;
912
        changeAddress = null;
913
    } else if (typeof allowZeroConf === "function") {
914
        cb = allowZeroConf;
915
        allowZeroConf = false;
916
    } else if (typeof randomizeChangeIdx === "function") {
917
        cb = randomizeChangeIdx;
918
        randomizeChangeIdx = true;
919
    } else if (typeof feeStrategy === "function") {
920
        cb = feeStrategy;
921
        feeStrategy = null;
922
    } else if (typeof twoFactorToken === "function") {
923
        cb = twoFactorToken;
924
        twoFactorToken = null;
925
    } else if (typeof options === "function") {
926
        cb = options;
927
        options = {};
928
    }
929
930
    randomizeChangeIdx = typeof randomizeChangeIdx !== "undefined" ? randomizeChangeIdx : true;
931
    feeStrategy = feeStrategy || Wallet.FEE_STRATEGY_OPTIMAL;
932
    options = options || {};
933
    var checkFee = typeof options.checkFee !== "undefined" ? options.checkFee : true;
934
935
    var deferred = q.defer();
936
    deferred.promise.nodeify(cb);
937
938
    if (self.locked) {
939
        deferred.reject(new blocktrail.WalletLockedError("Wallet needs to be unlocked to send coins"));
940
        return deferred.promise;
941
    }
942
943
    q.nextTick(function() {
944
        deferred.notify(Wallet.PAY_PROGRESS_START);
945
        self.buildTransaction(pay, changeAddress, allowZeroConf, randomizeChangeIdx, feeStrategy, options)
946
            .then(
947
            function(r) { return r; },
948
            function(e) { deferred.reject(e); },
949
            function(progress) {
950
                deferred.notify(progress);
951
            }
952
        )
953
            .spread(
954
            function(tx, utxos) {
955
956
                deferred.notify(Wallet.PAY_PROGRESS_SEND);
957
958
                var data = {
959
                    signed_transaction: tx.toHex(),
960
                    base_transaction: tx.__toBuffer(null, null, false).toString('hex')
961
                };
962
963
                return self.sendTransaction(data, utxos.map(function(utxo) { return utxo['path']; }), checkFee, twoFactorToken, options.prioboost)
964
                    .then(function(result) {
965
                        deferred.notify(Wallet.PAY_PROGRESS_DONE);
966
967
                        if (!result || !result['complete'] || result['complete'] === 'false') {
968
                            deferred.reject(new blocktrail.TransactionSignError("Failed to completely sign transaction"));
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
969
                        } else {
970
                            return result['txid'];
971
                        }
972
                    });
973
            },
974
            function(e) {
975
                throw e;
976
            }
977
        )
978
            .then(
979
            function(r) { deferred.resolve(r); },
980
            function(e) { deferred.reject(e); }
981
        )
982
        ;
983
    });
984
985
    return deferred.promise;
986
};
987
988
Wallet.prototype.decodeAddress = function(address) {
989
    return Wallet.getAddressAndType(address, this.network, this.useNewCashAddr);
990
};
991
992
function readBech32Address(address, network) {
993
    var addr;
994
    var err;
995
    try {
996
        addr = bitcoin.address.fromBech32(address, network);
997
        err = null;
998
999
    } catch (_err) {
1000
        err = _err;
1001
    }
1002
1003
    if (!err) {
1004
        // Valid bech32 but invalid network immediately alerts
1005
        if (addr.prefix !== network.bech32) {
1006
            throw new blocktrail.InvalidAddressError("Address invalid on this network");
1007
        }
1008
    }
1009
1010
    return [err, addr];
1011
}
1012
1013
function readCashAddress(address, network) {
1014
    var addr;
1015
    var err;
1016
    try {
1017
        addr = bitcoin.address.fromCashAddress(address);
1018
        err = null;
1019
    } catch (_err) {
1020
        err = _err;
1021
    }
1022
1023
    if (!err) {
1024
        // Valid base58 but invalid network immediately alerts
1025
        if (addr.prefix !== network.cashAddrPrefix) {
1026
            throw new Error(address + ' has an invalid prefix');
1027
        }
1028
    }
1029
1030
    return [err, addr];
1031
}
1032
1033
function readBase58Address(address, network) {
1034
    var addr;
1035
    var err;
1036
    try {
1037
        addr = bitcoin.address.fromBase58Check(address);
1038
        err = null;
1039
    } catch (_err) {
1040
        err = _err;
1041
    }
1042
1043
    if (!err) {
1044
        // Valid base58 but invalid network immediately alerts
1045
        if (addr.version !== network.pubKeyHash && addr.version !== network.scriptHash) {
1046
            throw new blocktrail.InvalidAddressError("Address invalid on this network");
1047
        }
1048
    }
1049
1050
    return [err, addr];
1051
}
1052
1053
Wallet.getAddressAndType = function(address, network, allowCashAddress) {
1054
    var addr;
1055
    var type;
1056
    var err;
1057
1058
    function readAddress(reader, readType) {
1059
        var decoded = reader(address, network);
1060
        if (decoded[0] === null) {
1061
            addr = decoded[1];
1062
            type = readType;
1063
        } else {
1064
            err = decoded[0];
1065
        }
1066
    }
1067
1068
    if (network === bitcoin.networks.bitcoin || network === bitcoin.networks.testnet) {
1069
        readAddress(readBech32Address, "bech32");
1070
    }
1071
1072
    if (!addr && 'cashAddrPrefix' in network && allowCashAddress) {
1073
        readAddress(readCashAddress, "cashaddr");
1074
    }
1075
1076
    if (!addr) {
1077
        readAddress(readBase58Address, "base58");
1078
    }
1079
1080
    if (addr) {
1081
        return {
1082
            address: address,
1083
            decoded: addr,
1084
            type: type
1085
        };
1086
    } else {
1087
        throw new blocktrail.InvalidAddressError(err.message);
1088
    }
1089
};
1090
1091
Wallet.convertPayToOutputs = function(pay, network, allowCashAddr) {
1092
    var send = [];
1093
1094
    var readFunc;
1095
1096
    // Deal with two different forms
1097
    if (Array.isArray(pay)) {
1098
        // output[]
1099
        readFunc = function(i, output, obj) {
1100
            if (typeof output !== "object") {
1101
                throw new Error("Invalid transaction output for numerically indexed list [1]");
1102
            }
1103
1104
            var keys = Object.keys(output);
1105
            if (keys.indexOf("scriptPubKey") !== -1 && keys.indexOf("value") !== -1) {
1106
                obj.scriptPubKey = output["scriptPubKey"];
1107
                obj.value = output["value"];
1108
            } else if (keys.indexOf("address") !== -1 && keys.indexOf("value") !== -1) {
1109
                obj.address = output["address"];
1110
                obj.value = output["value"];
1111
            } else if (keys.length === 2 && output.length === 2 && keys[0] === '0' && keys[1] === '1') {
1112
                obj.address = output[0];
1113
                obj.value = output[1];
1114
            } else {
1115
                throw new Error("Invalid transaction output for numerically indexed list [2]");
1116
            }
1117
        };
1118
    } else if (typeof pay === "object") {
1119
        // map[addr]amount
1120
        readFunc = function(address, value, obj) {
1121
            obj.address = address.trim();
1122
            obj.value = value;
1123
            if (obj.address === Wallet.OP_RETURN) {
1124
                var datachunk = Buffer.isBuffer(value) ? value : new Buffer(value, 'utf-8');
0 ignored issues
show
Bug introduced by
The variable Buffer seems to be never declared. If this is a global, consider adding a /** global: Buffer */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
1125
                obj.scriptPubKey = bitcoin.script.nullData.output.encode(datachunk).toString('hex');
1126
                obj.value = 0;
1127
                obj.address = null;
1128
            }
1129
        };
1130
    } else {
1131
        throw new Error("Invalid input");
1132
    }
1133
1134
    Object.keys(pay).forEach(function(key) {
1135
        var obj = {};
1136
        readFunc(key, pay[key], obj);
1137
1138
        if (parseInt(obj.value, 10).toString() !== obj.value.toString()) {
1139
            throw new blocktrail.WalletSendError("Values should be in Satoshis");
1140
        }
1141
1142
        // Remove address, replace with scriptPubKey
1143
        if (typeof obj.address === "string") {
1144
            try {
1145
                var addrAndType = Wallet.getAddressAndType(obj.address, network, allowCashAddr);
1146
                obj.scriptPubKey = bitcoin.address.toOutputScript(addrAndType.address, network, allowCashAddr).toString('hex');
1147
                delete obj.address;
1148
            } catch (e) {
1149
                throw new blocktrail.InvalidAddressError("Invalid address [" + obj.address + "] (" + e.message + ")");
1150
            }
1151
        }
1152
1153
        // Extra checks when the output isn't OP_RETURN
1154
        if (obj.scriptPubKey.slice(0, 2) !== "6a") {
1155
            if (!(obj.value = parseInt(obj.value, 10))) {
1156
                throw new blocktrail.WalletSendError("Values should be non zero");
1157
            } else if (obj.value <= blocktrail.DUST) {
1158
                throw new blocktrail.WalletSendError("Values should be more than dust (" + blocktrail.DUST + ")");
1159
            }
1160
        }
1161
1162
        // Value fully checked now
1163
        obj.value = parseInt(obj.value, 10);
1164
1165
        send.push(obj);
1166
    });
1167
1168
    return send;
1169
};
1170
1171
Wallet.prototype.buildTransaction = function(pay, changeAddress, allowZeroConf, randomizeChangeIdx, feeStrategy, options, cb) {
1172
    /* jshint -W071 */
1173
    var self = this;
1174
1175
    if (typeof changeAddress === "function") {
1176
        cb = changeAddress;
1177
        changeAddress = null;
1178
    } else if (typeof allowZeroConf === "function") {
1179
        cb = allowZeroConf;
1180
        allowZeroConf = false;
1181
    } else if (typeof randomizeChangeIdx === "function") {
1182
        cb = randomizeChangeIdx;
1183
        randomizeChangeIdx = true;
1184
    } else if (typeof feeStrategy === "function") {
1185
        cb = feeStrategy;
1186
        feeStrategy = null;
1187
    } else if (typeof options === "function") {
1188
        cb = options;
1189
        options = {};
1190
    }
1191
1192
    randomizeChangeIdx = typeof randomizeChangeIdx !== "undefined" ? randomizeChangeIdx : true;
1193
    feeStrategy = feeStrategy || Wallet.FEE_STRATEGY_OPTIMAL;
1194
    options = options || {};
1195
1196
    var deferred = q.defer();
1197
    deferred.promise.spreadNodeify(cb);
1198
1199
    q.nextTick(function() {
1200
        var send;
1201
        try {
1202
            send = Wallet.convertPayToOutputs(pay, self.network, self.useNewCashAddr);
1203
        } catch (e) {
1204
            deferred.reject(e);
1205
            return deferred.promise;
1206
        }
1207
1208
        if (!send.length) {
1209
            deferred.reject(new blocktrail.WalletSendError("Need at least one recipient"));
1210
            return deferred.promise;
1211
        }
1212
1213
        deferred.notify(Wallet.PAY_PROGRESS_COIN_SELECTION);
1214
1215
        deferred.resolve(
1216
            self.coinSelection(send, true, allowZeroConf, feeStrategy, options)
1217
            /**
1218
             *
1219
             * @param {Object[]} utxos
1220
             * @param fee
1221
             * @param change
1222
             * @param randomizeChangeIdx
0 ignored issues
show
Documentation introduced by
The parameter randomizeChangeIdx does not exist. Did you maybe forget to remove this comment?
Loading history...
1223
             * @returns {*}
1224
             */
1225
                .spread(function(utxos, fee, change) {
1226
                    var tx, txb, outputs = [];
1227
1228
                    var deferred = q.defer();
1229
1230
                    async.waterfall([
1231
                        /**
1232
                         * prepare
1233
                         *
1234
                         * @param cb
1235
                         */
1236
                        function(cb) {
1237
                            var inputsTotal = utxos.map(function(utxo) {
1238
                                return utxo['value'];
1239
                            }).reduce(function(a, b) {
1240
                                return a + b;
1241
                            });
1242
                            var outputsTotal = send.map(function(output) {
1243
                                return output.value;
1244
                            }).reduce(function(a, b) {
1245
                                return a + b;
1246
                            });
1247
                            var estimatedChange = inputsTotal - outputsTotal - fee;
1248
1249
                            if (estimatedChange > blocktrail.DUST * 2 && estimatedChange !== change) {
1250
                                return cb(new blocktrail.WalletFeeError("the amount of change (" + change + ") " +
1251
                                    "suggested by the coin selection seems incorrect (" + estimatedChange + ")"));
1252
                            }
1253
1254
                            cb();
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
1255
                        },
1256
                        /**
1257
                         * init transaction builder
1258
                         *
1259
                         * @param cb
1260
                         */
1261
                        function(cb) {
1262
                            txb = new bitcoin.TransactionBuilder(self.network);
1263
                            if (self.bitcoinCash) {
1264
                                txb.enableBitcoinCash();
1265
                            }
1266
1267
                            cb();
1268
                        },
1269
                        /**
1270
                         * add UTXOs as inputs
1271
                         *
1272
                         * @param cb
1273
                         */
1274
                        function(cb) {
1275
                            var i;
1276
1277
                            for (i = 0; i < utxos.length; i++) {
1278
                                txb.addInput(utxos[i]['hash'], utxos[i]['idx']);
1279
                            }
1280
1281
                            cb();
1282
                        },
1283
                        /**
1284
                         * build desired outputs
1285
                         *
1286
                         * @param cb
1287
                         */
1288
                        function(cb) {
1289
                            send.forEach(function(_send) {
1290
                                if (_send.address) {
1291
                                    outputs.push({address: _send.address, value: _send.value});
1292
                                } else if (_send.scriptPubKey) {
1293
                                    outputs.push({scriptPubKey: new Buffer(_send.scriptPubKey, 'hex'), value: _send.value});
0 ignored issues
show
Bug introduced by
The variable Buffer seems to be never declared. If this is a global, consider adding a /** global: Buffer */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
1294
                                } else {
1295
                                    throw new Error("Invalid send");
1296
                                }
1297
                            });
1298
                            cb();
1299
                        },
1300
                        /**
1301
                         * get change address if required
1302
                         *
1303
                         * @param cb
1304
                         */
1305
                        function(cb) {
1306
                            if (change > 0) {
1307
                                if (change <= blocktrail.DUST) {
1308
                                    change = 0; // don't do a change output if it would be a dust output
1309
1310
                                } else {
1311
                                    if (!changeAddress) {
1312
                                        deferred.notify(Wallet.PAY_PROGRESS_CHANGE_ADDRESS);
1313
1314
                                        return self.getNewAddress(self.changeChain, function(err, address) {
1315
                                            if (err) {
1316
                                                return cb(err);
1317
                                            }
1318
                                            changeAddress = address;
1319
                                            cb();
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
1320
                                        });
1321
                                    }
1322
                                }
1323
                            }
1324
1325
                            cb();
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
1326
                        },
1327
                        /**
1328
                         * add change to outputs
1329
                         *
1330
                         * @param cb
1331
                         */
1332
                        function(cb) {
1333
                            if (change > 0) {
1334
                                if (randomizeChangeIdx) {
1335
                                    outputs.splice(_.random(0, outputs.length), 0, {
1336
                                        address: changeAddress,
1337
                                        value: change
1338
                                    });
1339
                                } else {
1340
                                    outputs.push({address: changeAddress, value: change});
1341
                                }
1342
                            }
1343
1344
                            cb();
1345
                        },
1346
                        /**
1347
                         * add outputs to txb
1348
                         *
1349
                         * @param cb
1350
                         */
1351
                        function(cb) {
1352
                            outputs.forEach(function(outputInfo) {
1353
                                txb.addOutput(outputInfo.scriptPubKey || outputInfo.address, outputInfo.value);
1354
                            });
1355
1356
                            cb();
1357
                        },
1358
                        /**
1359
                         * sign
1360
                         *
1361
                         * @param cb
1362
                         */
1363
                        function(cb) {
1364
                            var i, privKey, path, redeemScript, witnessScript;
1365
1366
                            deferred.notify(Wallet.PAY_PROGRESS_SIGN);
1367
1368
                            for (i = 0; i < utxos.length; i++) {
1369
                                var mode = SignMode.SIGN;
1370
                                if (utxos[i].sign_mode) {
1371
                                    mode = utxos[i].sign_mode;
1372
                                }
1373
1374
                                redeemScript = null;
0 ignored issues
show
Unused Code introduced by
The assignment to redeemScript seems to be never used. If you intend to free memory here, this is not necessary since the variable leaves the scope anyway.
Loading history...
1375
                                witnessScript = null;
1376
                                if (mode === SignMode.SIGN) {
1377
                                    path = utxos[i]['path'].replace("M", "m");
1378
1379
                                    // todo: regenerate scripts for path and compare for utxo (paranoid mode)
1380
                                    if (self.primaryPrivateKey) {
1381
                                        privKey = Wallet.deriveByPath(self.primaryPrivateKey, path, "m").keyPair;
1382
                                    } else if (self.backupPrivateKey) {
1383
                                        privKey = Wallet.deriveByPath(self.backupPrivateKey, path.replace(/^m\/(\d+)\'/, 'm/$1'), "m").keyPair;
1384
                                    } else {
1385
                                        throw new Error("No master privateKey present");
1386
                                    }
1387
1388
                                    redeemScript = new Buffer(utxos[i]['redeem_script'], 'hex');
0 ignored issues
show
Bug introduced by
The variable Buffer seems to be never declared. If this is a global, consider adding a /** global: Buffer */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
1389
                                    if (typeof utxos[i]['witness_script'] === 'string') {
1390
                                        witnessScript = new Buffer(utxos[i]['witness_script'], 'hex');
1391
                                    }
1392
1393
                                    var sigHash = bitcoin.Transaction.SIGHASH_ALL;
1394
                                    if (self.bitcoinCash) {
1395
                                        sigHash |= bitcoin.Transaction.SIGHASH_BITCOINCASHBIP143;
1396
                                    }
1397
1398
                                    txb.sign(i, privKey, redeemScript, sigHash, utxos[i].value, witnessScript);
1399
                                }
1400
                            }
1401
1402
                            tx = txb.buildIncomplete();
1403
1404
                            cb();
1405
                        },
1406
                        /**
1407
                         * estimate fee to verify that the API is not providing us wrong data
1408
                         *
1409
                         * @param cb
1410
                         */
1411
                        function(cb) {
1412
                            var estimatedFee = Wallet.estimateVsizeFee(tx, utxos);
1413
1414
                            if (self.sdk.feeSanityCheck) {
1415
                                switch (feeStrategy) {
0 ignored issues
show
Coding Style introduced by
As per coding-style, switch statements should have a default case.
Loading history...
1416
                                    case Wallet.FEE_STRATEGY_BASE_FEE:
0 ignored issues
show
Bug introduced by
The variable Wallet seems to be never declared. If this is a global, consider adding a /** global: Wallet */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
1417
                                        if (Math.abs(estimatedFee - fee) > blocktrail.BASE_FEE) {
1418
                                            return cb(new blocktrail.WalletFeeError("the fee suggested by the coin selection (" + fee + ") " +
1419
                                                "seems incorrect (" + estimatedFee + ") for FEE_STRATEGY_BASE_FEE"));
1420
                                        }
1421
                                    break;
1422
1423
                                    case Wallet.FEE_STRATEGY_HIGH_PRIORITY:
1424
                                    case Wallet.FEE_STRATEGY_OPTIMAL:
1425
                                    case Wallet.FEE_STRATEGY_LOW_PRIORITY:
1426
                                        if (fee > estimatedFee * self.feeSanityCheckBaseFeeMultiplier) {
1427
                                            return cb(new blocktrail.WalletFeeError("the fee suggested by the coin selection (" + fee + ") " +
1428
                                                "seems awefully high (" + estimatedFee + ") for FEE_STRATEGY_OPTIMAL"));
1429
                                        }
1430
                                    break;
1431
                                }
1432
                            }
1433
1434
                            cb();
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
1435
                        }
1436
                    ], function(err) {
1437
                        if (err) {
1438
                            deferred.reject(new blocktrail.WalletSendError(err));
1439
                            return;
1440
                        }
1441
1442
                        deferred.resolve([tx, utxos]);
1443
                    });
1444
1445
                    return deferred.promise;
1446
                }
1447
            )
1448
        );
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
1449
    });
1450
1451
    return deferred.promise;
1452
};
1453
1454
1455
/**
1456
 * use the API to get the best inputs to use based on the outputs
1457
 *
1458
 * @param pay               array       {'address': (int)value}     coins to send
1459
 * @param [lockUTXO]        bool        lock UTXOs for a few seconds to allow for transaction to be created
1460
 * @param [allowZeroConf]   bool        allow zero confirmation unspent outputs to be used in coin selection
1461
 * @param [feeStrategy]     string      defaults to FEE_STRATEGY_OPTIMAL
1462
 * @param [options]         object
1463
 * @param [cb]              function    callback(err, utxos, fee, change)
1464
 * @returns {q.Promise}
1465
 */
1466
Wallet.prototype.coinSelection = function(pay, lockUTXO, allowZeroConf, feeStrategy, options, cb) {
1467
    var self = this;
1468
1469
    if (typeof lockUTXO === "function") {
1470
        cb = lockUTXO;
1471
        lockUTXO = true;
1472
    } else if (typeof allowZeroConf === "function") {
1473
        cb = allowZeroConf;
1474
        allowZeroConf = false;
1475
    } else if (typeof feeStrategy === "function") {
1476
        cb = feeStrategy;
1477
        feeStrategy = null;
1478
    } else if (typeof options === "function") {
1479
        cb = options;
1480
        options = {};
1481
    }
1482
1483
    lockUTXO = typeof lockUTXO !== "undefined" ? lockUTXO : true;
1484
    feeStrategy = feeStrategy || Wallet.FEE_STRATEGY_OPTIMAL;
1485
    options = options || {};
1486
1487
    var send;
1488
    try {
1489
        send = Wallet.convertPayToOutputs(pay, self.network, self.useNewCashAddr);
1490
    } catch (e) {
1491
        var deferred = q.defer();
1492
        deferred.promise.nodeify(cb);
1493
        deferred.reject(e);
1494
        return deferred.promise;
1495
    }
1496
1497
    return self.sdk.coinSelection(self.identifier, send, lockUTXO, allowZeroConf, feeStrategy, options, cb);
1498
};
1499
1500
/**
1501
 * send the transaction using the API
1502
 *
1503
 * @param txHex             string      partially signed transaction as hex string
1504
 * @param paths             array       list of paths used in inputs which should be cosigned by the API
1505
 * @param checkFee          bool        when TRUE the API will verify if the fee is 100% correct and otherwise throw an exception
1506
 * @param [twoFactorToken]  string      2FA token
1507
 * @param prioboost         bool
1508
 * @param [cb]              function    callback(err, txHash)
1509
 * @returns {q.Promise}
1510
 */
1511
Wallet.prototype.sendTransaction = function(txHex, paths, checkFee, twoFactorToken, prioboost, cb) {
1512
    var self = this;
1513
1514
    if (typeof twoFactorToken === "function") {
1515
        cb = twoFactorToken;
1516
        twoFactorToken = null;
1517
        prioboost = false;
1518
    } else if (typeof prioboost === "function") {
1519
        cb = twoFactorToken;
1520
        prioboost = false;
1521
    }
1522
1523
    var deferred = q.defer();
1524
    deferred.promise.nodeify(cb);
1525
1526
    self.sdk.sendTransaction(self.identifier, txHex, paths, checkFee, twoFactorToken, prioboost)
1527
        .then(
1528
            function(result) {
1529
                deferred.resolve(result);
1530
            },
1531
            function(e) {
1532
                if (e.requires_2fa) {
1533
                    deferred.reject(new blocktrail.WalletMissing2FAError());
1534
                } else if (e.message.match(/Invalid two_factor_token/)) {
1535
                    deferred.reject(new blocktrail.WalletInvalid2FAError());
1536
                } else {
1537
                    deferred.reject(e);
1538
                }
1539
            }
1540
        )
1541
    ;
1542
1543
    return deferred.promise;
1544
};
1545
1546
/**
1547
 * setup a webhook for this wallet
1548
 *
1549
 * @param url           string      URL to receive webhook events
1550
 * @param [identifier]  string      identifier for the webhook, defaults to WALLET- + wallet.identifier
1551
 * @param [cb]          function    callback(err, webhook)
1552
 * @returns {q.Promise}
1553
 */
1554
Wallet.prototype.setupWebhook = function(url, identifier, cb) {
1555
    var self = this;
1556
1557
    if (typeof identifier === "function") {
1558
        cb = identifier;
1559
        identifier = null;
1560
    }
1561
1562
    identifier = identifier || ('WALLET-' + self.identifier);
1563
1564
    return self.sdk.setupWalletWebhook(self.identifier, identifier, url, cb);
1565
};
1566
1567
/**
1568
 * delete a webhook that was created for this wallet
1569
 *
1570
 * @param [identifier]  string      identifier for the webhook, defaults to WALLET- + wallet.identifier
1571
 * @param [cb]          function    callback(err, success)
1572
 * @returns {q.Promise}
1573
 */
1574
Wallet.prototype.deleteWebhook = function(identifier, cb) {
1575
    var self = this;
1576
1577
    if (typeof identifier === "function") {
1578
        cb = identifier;
1579
        identifier = null;
1580
    }
1581
1582
    identifier = identifier || ('WALLET-' + self.identifier);
1583
1584
    return self.sdk.deleteWalletWebhook(self.identifier, identifier, cb);
1585
};
1586
1587
/**
1588
 * get all transactions for the wallet (paginated)
1589
 *
1590
 * @param [params]  object      pagination: {page: 1, limit: 20, sort_dir: 'asc'}
1591
 * @param [cb]      function    callback(err, transactions)
1592
 * @returns {q.Promise}
1593
 */
1594
Wallet.prototype.transactions = function(params, cb) {
1595
    var self = this;
1596
1597
    return self.sdk.walletTransactions(self.identifier, params, cb);
1598
};
1599
1600
Wallet.prototype.maxSpendable = function(allowZeroConf, feeStrategy, options, cb) {
1601
    var self = this;
1602
1603
    if (typeof allowZeroConf === "function") {
1604
        cb = allowZeroConf;
1605
        allowZeroConf = false;
1606
    } else if (typeof feeStrategy === "function") {
1607
        cb = feeStrategy;
1608
        feeStrategy = null;
1609
    } else if (typeof options === "function") {
1610
        cb = options;
1611
        options = {};
1612
    }
1613
1614
    if (typeof allowZeroConf === "object") {
1615
        options = allowZeroConf;
1616
        allowZeroConf = false;
1617
    } else if (typeof feeStrategy === "object") {
1618
        options = feeStrategy;
1619
        feeStrategy = null;
1620
    }
1621
1622
    options = options || {};
1623
1624
    if (typeof options.allowZeroConf !== "undefined") {
1625
        allowZeroConf = options.allowZeroConf;
1626
    }
1627
    if (typeof options.feeStrategy !== "undefined") {
1628
        feeStrategy = options.feeStrategy;
1629
    }
1630
1631
    feeStrategy = feeStrategy || Wallet.FEE_STRATEGY_OPTIMAL;
1632
1633
    return self.sdk.walletMaxSpendable(self.identifier, allowZeroConf, feeStrategy, options, cb);
1634
};
1635
1636
/**
1637
 * get all addresses for the wallet (paginated)
1638
 *
1639
 * @param [params]  object      pagination: {page: 1, limit: 20, sort_dir: 'asc'}
1640
 * @param [cb]      function    callback(err, addresses)
1641
 * @returns {q.Promise}
1642
 */
1643
Wallet.prototype.addresses = function(params, cb) {
1644
    var self = this;
1645
1646
    return self.sdk.walletAddresses(self.identifier, params, cb);
1647
};
1648
1649
/**
1650
 * @param address   string      the address to label
1651
 * @param label     string      the label
1652
 * @param [cb]      function    callback(err, res)
1653
 * @returns {q.Promise}
1654
 */
1655
Wallet.prototype.labelAddress = function(address, label, cb) {
1656
    var self = this;
1657
1658
    return self.sdk.labelWalletAddress(self.identifier, address, label, cb);
1659
};
1660
1661
/**
1662
 * get all UTXOs for the wallet (paginated)
1663
 *
1664
 * @param [params]  object      pagination: {page: 1, limit: 20, sort_dir: 'asc'}
1665
 * @param [cb]      function    callback(err, addresses)
1666
 * @returns {q.Promise}
1667
 */
1668
Wallet.prototype.utxos = function(params, cb) {
1669
    var self = this;
1670
1671
    return self.sdk.walletUTXOs(self.identifier, params, cb);
1672
};
1673
1674
Wallet.prototype.unspentOutputs = Wallet.prototype.utxos;
1675
1676
/**
1677
 * sort list of pubkeys to be used in a multisig redeemscript
1678
 *  sorted in lexicographical order on the hex of the pubkey
1679
 *
1680
 * @param pubKeys   {bitcoin.HDNode[]}
1681
 * @returns string[]
1682
 */
1683
Wallet.sortMultiSigKeys = function(pubKeys) {
1684
    pubKeys.sort(function(key1, key2) {
1685
        return key1.toString('hex').localeCompare(key2.toString('hex'));
1686
    });
1687
1688
    return pubKeys;
1689
};
1690
1691
/**
1692
 * determine how much fee is required based on the inputs and outputs
1693
 *  this is an estimation, not a proper 100% correct calculation
1694
 *
1695
 * @todo: mark deprecated in favor of estimations where UTXOS are known
1696
 * @param {bitcoin.Transaction} tx
1697
 * @param {int} feePerKb when not null use this feePerKb, otherwise use BASE_FEE legacy calculation
1698
 * @returns {number}
1699
 */
1700
Wallet.estimateIncompleteTxFee = function(tx, feePerKb) {
1701
    var size = Wallet.estimateIncompleteTxSize(tx);
1702
    var sizeKB = size / 1000;
1703
    var sizeKBCeil = Math.ceil(size / 1000);
1704
1705
    if (feePerKb) {
1706
        return parseInt(sizeKB * feePerKb, 10);
1707
    } else {
1708
        return parseInt(sizeKBCeil * blocktrail.BASE_FEE, 10);
1709
    }
1710
};
1711
1712
/**
1713
 * Takes tx and utxos, computing their estimated vsize,
1714
 * and uses feePerKb (or BASEFEE as default) to estimate
1715
 * the number of satoshis in fee.
1716
 *
1717
 * @param {bitcoin.Transaction} tx
1718
 * @param {Array} utxos
1719
 * @param feePerKb
1720
 * @returns {Number}
1721
 */
1722
Wallet.estimateVsizeFee = function(tx, utxos, feePerKb) {
1723
    var vsize = SizeEstimation.estimateTxVsize(tx, utxos);
1724
    var sizeKB = vsize / 1000;
1725
    var sizeKBCeil = Math.ceil(vsize / 1000);
1726
1727
    if (feePerKb) {
1728
        return parseInt(sizeKB * feePerKb, 10);
1729
    } else {
1730
        return parseInt(sizeKBCeil * blocktrail.BASE_FEE, 10);
1731
    }
1732
};
1733
1734
/**
1735
 * determine how much fee is required based on the inputs and outputs
1736
 *  this is an estimation, not a proper 100% correct calculation
1737
 *
1738
 * @param {bitcoin.Transaction} tx
1739
 * @returns {number}
1740
 */
1741
Wallet.estimateIncompleteTxSize = function(tx) {
1742
    var size = 4 + 4 + 4 + 4; // version + txinVarInt + txoutVarInt + locktime
1743
1744
    size += tx.outs.length * 34;
1745
1746
    tx.ins.forEach(function(txin) {
1747
        var scriptSig = txin.script,
1748
            scriptType = bitcoin.script.classifyInput(scriptSig);
1749
1750
        var multiSig = [2, 3]; // tmp hardcoded
1751
1752
        // Re-classify if P2SH
1753
        if (!multiSig && scriptType === 'scripthash') {
1754
            var sigChunks = bitcoin.script.decompile(scriptSig);
1755
            var redeemScript = sigChunks.slice(-1)[0];
1756
            scriptSig = bitcoin.script.compile(sigChunks.slice(0, -1));
1757
            scriptType = bitcoin.script.classifyInput(scriptSig);
1758
1759
            if (bitcoin.script.classifyOutput(redeemScript) !== scriptType) {
1760
                throw new blocktrail.TransactionInputError('Non-matching scriptSig and scriptPubKey in input');
1761
            }
1762
1763
            // figure out M of N for multisig (code from internal usage of bitcoinjs)
1764
            if (scriptType === 'multisig') {
1765
                var rsChunks = bitcoin.script.decompile(redeemScript);
1766
                var mOp = rsChunks[0];
1767
                if (mOp === bitcoin.opcodes.OP_0 || mOp < bitcoin.opcodes.OP_1 || mOp > bitcoin.opcodes.OP_16) {
1768
                    throw new blocktrail.TransactionInputError("Invalid multisig redeemScript");
1769
                }
1770
1771
                var nOp = rsChunks[redeemScript.chunks.length - 2];
1772
                if (mOp === bitcoin.opcodes.OP_0 || mOp < bitcoin.opcodes.OP_1 || mOp > bitcoin.opcodes.OP_16) {
1773
                    throw new blocktrail.TransactionInputError("Invalid multisig redeemScript");
1774
                }
1775
1776
                var m = mOp - (bitcoin.opcodes.OP_1 - 1);
1777
                var n = nOp - (bitcoin.opcodes.OP_1 - 1);
1778
                if (n < m) {
1779
                    throw new blocktrail.TransactionInputError("Invalid multisig redeemScript");
1780
                }
1781
1782
                multiSig = [m, n];
1783
            }
1784
        }
1785
1786
        if (multiSig) {
1787
            size += (
1788
                32 + // txhash
1789
                4 + // idx
1790
                3 + // scriptVarInt[>=253]
1791
                1 + // OP_0
1792
                ((1 + 72) * multiSig[0]) + // (OP_PUSHDATA[<75] + 72) * sigCnt
1793
                (2 + 105) + // OP_PUSHDATA[>=75] + script
1794
                4 // sequence
1795
            );
1796
1797
        } else {
1798
            size += 32 + // txhash
1799
                4 + // idx
1800
                73 + // sig
1801
                34 + // script
1802
                4; // sequence
1803
        }
1804
    });
1805
1806
    return size;
1807
};
1808
1809
/**
1810
 * determine how much fee is required based on the amount of inputs and outputs
1811
 *  this is an estimation, not a proper 100% correct calculation
1812
 *  this asumes all inputs are 2of3 multisig
1813
 *
1814
 * @todo: mark deprecated in favor of situations where UTXOS are known
1815
 * @param txinCnt       {number}
1816
 * @param txoutCnt      {number}
1817
 * @returns {number}
1818
 */
1819
Wallet.estimateFee = function(txinCnt, txoutCnt) {
1820
    var size = 4 + 4 + 4 + 4; // version + txinVarInt + txoutVarInt + locktime
1821
1822
    size += txoutCnt * 34;
1823
1824
    size += (
1825
            32 + // txhash
1826
            4 + // idx
1827
            3 + // scriptVarInt[>=253]
1828
            1 + // OP_0
1829
            ((1 + 72) * 2) + // (OP_PUSHDATA[<75] + 72) * sigCnt
1830
            (2 + 105) + // OP_PUSHDATA[>=75] + script
1831
            4 // sequence
1832
        ) * txinCnt;
1833
1834
    var sizeKB = Math.ceil(size / 1000);
1835
1836
    return sizeKB * blocktrail.BASE_FEE;
1837
};
1838
1839
/**
1840
 * create derived key from parent key by path
1841
 *
1842
 * @param hdKey     {bitcoin.HDNode}
1843
 * @param path      string
1844
 * @param keyPath   string
1845
 * @returns {bitcoin.HDNode}
1846
 */
1847
Wallet.deriveByPath = function(hdKey, path, keyPath) {
1848
    keyPath = keyPath || (!!hdKey.keyPair.d ? "m" : "M");
1849
1850
    if (path[0].toLowerCase() !== "m" || keyPath[0].toLowerCase() !== "m") {
1851
        throw new blocktrail.KeyPathError("Wallet.deriveByPath only works with absolute paths. (" + path + ", " + keyPath + ")");
1852
    }
1853
1854
    if (path[0] === "m" && keyPath[0] === "M") {
1855
        throw new blocktrail.KeyPathError("Wallet.deriveByPath can't derive private path from public parent. (" + path + ", " + keyPath + ")");
1856
    }
1857
1858
    // if the desired path is public while the input is private
1859
    var toPublic = path[0] === "M" && keyPath[0] === "m";
1860
    if (toPublic) {
1861
        // derive the private path, convert to public when returning
1862
        path[0] = "m";
1863
    }
1864
1865
    // keyPath should be the parent parent of path
1866
    if (path.toLowerCase().indexOf(keyPath.toLowerCase()) !== 0) {
1867
        throw new blocktrail.KeyPathError("Wallet.derivePath requires path (" + path + ") to be a child of keyPath (" + keyPath + ")");
1868
    }
1869
1870
    // remove the part of the path we already have
1871
    path = path.substr(keyPath.length);
1872
1873
    // iterate over the chunks and derive
1874
    var newKey = hdKey;
1875
    path.replace(/^\//, "").split("/").forEach(function(chunk) {
1876
        if (!chunk) {
1877
            return;
1878
        }
1879
1880
        if (chunk.indexOf("'") !== -1) {
1881
            chunk = parseInt(chunk.replace("'", ""), 10) + bitcoin.HDNode.HIGHEST_BIT;
1882
        }
1883
1884
        newKey = newKey.derive(parseInt(chunk, 10));
1885
    });
1886
1887
    if (toPublic) {
1888
        return newKey.neutered();
1889
    } else {
1890
        return newKey;
1891
    }
1892
};
1893
1894
module.exports = Wallet;
1895